{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 数值修约"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 简介"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Math.round 很多中文文档说是取四舍五入的整数。但这个说法是不正确的。我们分别看一下 Python 和 JS 里的 round 的函数返回："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Python"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "print(round(20.5)) # 20\n",
    "print(round(20.51)) # 21\n",
    "print(round(-20.5)) # -20\n",
    "print(round(-20.51)) # -21\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## JS"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```javascript\n",
    "console.log(Math.round(20.5)) // 21\n",
    "console.log(Math.round(20.51)) // 21\n",
    "console.log(Math.round(-20.5)) // -20\n",
    "console.log(Math.round(-20.51)) // -21\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "是不是感觉三观被颠覆？两个语言不一样，我们可以理解各自定义就不一样。但是为什么同样是 JS 里，`round(20.51) = 21` ,`round(-20.51) = -21`, `round(-20.5)` 又是 `-20` 呢？ "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们不说 Python，只说 JS 里的定义，我们去翻 [tc39的标准](https://tc39.es/ecma262/#sec-math.round) :"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> Returns the Number value that is closest to x and is integral. If two integral Numbers are equally close to x, then the result is the Number value that is closer to +∞. If x is already integral, the result is x.\n",
    "When the Math.round method is called with argument x, the following steps are taken:\n",
    "> 1. Let n be ? ToNumber(x).\n",
    "> 2. If n is NaN, +∞𝔽, -∞𝔽, or an integral Number, return n.\n",
    "> 3. If n < 0.5𝔽 and n > +0𝔽, return +0𝔽.\n",
    "> 4. If n < +0𝔽 and n ≥ -0.5𝔽, return -0𝔽.\n",
    "> 5. Return the integral Number closest to n, preferring the Number closer to +∞ in the case of a tie.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我翻译一下：返回最接近$x$且为整数的数值。如果两个整数相等地接近x，则结果是更接近$+\\infty$的数值。如果$x$已经是整数，结果是$x$。\n",
    "当 `Math.round` 方法传入一个参数 $x$, 将执行以下步骤：\n",
    "1. 将 $x$ 隐式类型转化为数字 $n$\n",
    "2. 如果 $n$ 为 NaN / Infinity / -Infinity， 或者是整数，返回 $n$\n",
    "3. 如果 $+0 < n < 0.5$，返回 +0\n",
    "4. 如果 $-0.5 \\leq n < +0$, 返回 -0\n",
    "5. 返回最接近 n 的整数，如果和两个整数距离相等，返回更接近 $+\\infty$ 的那个\n",
    "\n",
    "综上，所以在自然数下，Math.round 的结果与我们理解的四舍五入一致，但在负数上要小心。\n",
    "关于为什么会有 +0 和 -0，又是要讲一堆，可以参看月影大大的[这篇文章](https://github.com/akira-cn/FE_You_dont_know/issues/10)。另外，还有 BigInt 里的 0n，但没有 -0n，这三个 0 在逻辑判断的时候都是 false。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 关于四舍五入"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们学校所学习的四舍五入，其实并不是 IEEE754 所使用的标准修约方式。这使得我们在计算一些值的时候有惊喜。IEEE754 使用的修约标准叫 [Round half to even](https://en.wikipedia.org/wiki/Rounding)，也称为高斯舍入法、银行家舍入法或四舍六入五成双法。这比四舍五入在累计误差时会更小。\n",
    "\n",
    "因为四舍五入，舍入的数为0时，舍后就是这个数本身，而1-9共9个数，5处于中间，如果5-9都进一，进一的概率是九分之五，而1-4舍去，概率是九分之四，在累加时会使整体误差偏大。\n",
    "\n",
    "四舍五入只在中文圈用得多，甚至被老外称为 Chinese Rounding。西方更多用 Bankers Rounding。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "银行家舍入法的具体算法：\n",
    "\n",
    "```\n",
    "四舍六入五考虑，五后非零就进一，\n",
    "五后为零看奇偶，五前为偶应舍去，五前为奇要进一\n",
    "```\n",
    "\n",
    "以下小数舍入两位结果：\n",
    "\n",
    "```\n",
    "0.466 -> 0.47\n",
    "0.46507 -> 0.46\n",
    "0.455 -> 0.46\n",
    "```\n",
    "\n",
    "## 事情并没有这么简单\n",
    "然而，你在实际测试的时候，发现 chrome 下完全不是这么一回事，掀桌：\n",
    "\n",
    "```\n",
    "0.125.toFixed(2) -> 0.13, Python3 是 0.12\n",
    "0.465.toFixed(2) -> 0.47\n",
    "10.465.toFixed(2) -> 10.46\n",
    "```\n",
    "\n",
    "只好又去查文档，发现 IEEE745 不光提供了 Round half to even 的方式，还提供了[ties away from zero](https://en.wikipedia.org/wiki/IEEE_754#Roundings_to_nearest) 的修约规则。再去查 [tc39 里 toFixed 的实现](https://tc39.es/ecma262/#sec-number.prototype.tofixed)，toFixed 的实现如下，直接翻译："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "NOTE 1\n",
    "\n",
    "toFixed返回包含此数值(https://tc39.es/ecma262/#number-value)的字符串，该数值用小数点后的小数定点表示法表示。如果分数位数无定义，则假设为0。具体来说，执行以下步骤：\n",
    "\n",
    "...1-6 步略，主要隐式类型转换、各种异常值和越界判断。\n",
    "\n",
    "7. 设 x 为实数\n",
    "8. 令 s 为空字符串 \"\"\n",
    "9. 如果 x < 0, 则\n",
    "    a. 令 s 为 \"-\".\n",
    "    b. 令 x = –x.\n",
    "10. 如果 x ≥ 10**21, 则\n",
    "    a. 令 m = ToString(x) (科学计数法数字)\n",
    "11. 否则 (x < 10**21)\n",
    "    a. 令 n 为一个整数，让 n ÷ 10**f – x 准确的数学值尽可能接近零。如果有两个这样 n 值，选择较大的 n。\n",
    "    b. 如果 n = 0, 令 m 为字符串 \"0\". 否则 , 令 m 为由 n 的十进制表示里的数组成的字符串（为了没有前导零）。\n",
    "    c. 如果 f ≠ 0, 则\n",
    "       i. 令 k 为 m 里的字符数目 .\n",
    "       ii. 如果 k ≤ f, 则\n",
    "            1. 令 z 为 f+1–k 个 ‘0’ 组成的字符串 .\n",
    "            2. 令 m 为 串联字符串 z 的 m 的结果 .\n",
    "            3. 令 k = f + 1.\n",
    "       iii. 令 a 为 m 的前 k–f 个字符，\n",
    "       iv. 令 b 为其余 f 个字符\n",
    "        v. 令 m 为 串联三个字符串 a, \".\", 和 b 的结果。\n",
    "12. 返回串联字符串 s 和 m 的结果。\n",
    "\n",
    "NOTE 2\n",
    "\n",
    " 对于某些值，toFixed 的输出可比 toString 的更精确，因为 toString 只打印区分相邻数字值的足够的有效数字。例如 ,\n",
    "\n",
    " (1000000000000000128).toString() 返回 \"1000000000000000100\",\n",
    "\n",
    " 而 (1000000000000000128).toFixed(0) 返回 \"1000000000000000128\".\n",
    "```\n",
    "\n",
    "[V8 的实现](https://github.com/nodejs/node/blob/master/deps/v8/src/builtins/builtins-number.cc#L66)的确遵照了这个规范。\n",
    "\n",
    "另外，小数并不是都能精确表示的，你可以给 toFixed 传一个很大的参数(最大 100 位)，查看这个值，比如\n",
    "\n",
    "```javascript\n",
    "0.125.toFixed(100)\n",
    "// \"0.1250000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"\n",
    "\n",
    "0.135.toFixed(100)\n",
    "\n",
    "// \"0.1350000000000000088817841970012523233890533447265625000000000000000000000000000000000000000000000000\"\n",
    "```\n",
    "我们可以看到 0.125 可以在 js 里精确表示，0.135 却不可以，另外你也可以使用 toPrecision 方法或者这个[在线网站](https://www.binaryconvert.com/convert_double.html)查看具体精度。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "好吧，总算搞明白了，在实际过程中一定要注意这个坑，特别是在换算金额等敏感数据时。欢迎[留言](https://github.com/easy-math/Math/issues/15)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
